package com.coupon.distribution.service.impl;

import com.alibaba.fastjson.JSON;
import com.coupon.common.constant.Constants;
import com.coupon.common.exception.CouponException;
import com.coupon.distribution.constant.CouponStatusEnum;
import com.coupon.distribution.entity.Coupon;
import com.coupon.distribution.service.RedisService;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.RandomUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.core.RedisOperations;
import org.springframework.data.redis.core.SessionCallback;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

/**
 * @author 王哲
 * @Contact 1121586359@qq.com
 * @ClassName RedisServiceImpl.java
 * @create 2023年06月26日 上午11:29
 * @Description RedisService实现
 * @Version V1.0
 */
@Service
@Slf4j
public class RedisServiceImpl implements RedisService {

    @Autowired
    private StringRedisTemplate redisTemplate;

    // 根据userId和状态找到缓存的优惠券列表数据
    @Override
    public List<Coupon> getCachedCoupons(Long userId, Integer status) {
        log.info("Get Coupons From Cache: {}, {}", userId, status);

        String redisKey = status2RedisKey(status, userId);

        // 从Redis中获取数据
        List<String> collect = redisTemplate.opsForHash().values(redisKey)
                .stream()
                .map(o -> JSON.parseObject(o.toString(), String.class))
                .collect(Collectors.toList());

        if (collect.size() <= 0) {
            saveEmptyCouponListToCache(userId, Collections.singletonList(status));
            return Collections.emptyList();
        }

        return collect.stream()
                .map(cs -> JSON.parseObject(cs, Coupon.class))
                .collect(Collectors.toList());
    }

    // 保存空的优惠券列表到缓存中
    @Override
    public void saveEmptyCouponListToCache(Long userId, List<Integer> status) {
        log.info("Save Empty List To Cache For User: {}, Status: {}",
                userId, status);

        // key是Coupon_id, value是序列化的Coupon
        HashMap<String, String> invalidCouponMap = new HashMap<>();
        invalidCouponMap.put("-1", JSON.toJSONString(Coupon.invalidCoupon()));

        // 使用SessionCallback 把数据命令放入到Redis的pipeline
        SessionCallback<Object> objectSessionCallback = new SessionCallback<Object>() {
            @Override
            public Object execute(RedisOperations redisOperations)
                    throws DataAccessException {

                status.forEach(s -> {
                    String redisKey = status2RedisKey(s, userId);
                    redisOperations.opsForHash().putAll(redisKey, invalidCouponMap);
                });

                return null;
            }
        };
        log.info("Pipeline Exe Result: {}",
                JSON.toJSONString(redisTemplate.executePipelined(objectSessionCallback)));
    }

    // 尝试从Cache中获取一个优惠券码
    @Override
    public String tryToAcquireCouponCodeFromCache(Integer templateId) {

        log.info("Try To Acquire Coupon Code: {}", templateId);

        String redisKey = String.format("%s%s",
                Constants.RedisPrefix.COUPON_TEMPLATE, templateId.toString());

        // 因为优惠券码不存在顺序关系, 左边pop或右边pop, 没有影响
        String couponCode = redisTemplate.opsForList().leftPop(redisKey);

        log.info("Acquire Coupon Code: {}, {}, {}",
                templateId, redisKey, couponCode);


        return couponCode;
    }

    // 将优惠券保存到Cache中
    @Override
    public Integer addCouponToCache(Long userId, List<Coupon> coupons, Integer status)
            throws CouponException {

        log.info("Add Coupon To Cache: {}, {}, {}",
                userId, JSON.toJSONString(coupons), status);

        Integer result = -1;
        // status 是USABLE, 代表是新增的优惠券
        // status 是USED, 代表是已使用的优惠券
        // status 是EXPIRED, 代表是已过期的优惠券
        CouponStatusEnum couponStatusEnum = CouponStatusEnum.of(status);

        switch (couponStatusEnum) {
            case USABLE:
                result = addCouponToCacheForUsable(userId, coupons);
                break;
            case USED:
                result = addCouponToCacheForUsed(userId, coupons);
                break;
            case EXPIRED:
                result = addCouponToCacheForExpired(userId, coupons);
                break;
        }

        return result;
    }

    /**
     * 新增过期优惠券到Cache中
     * @param userId
     * @param coupons
     * @return
     */
    private Integer addCouponToCacheForExpired(Long userId, List<Coupon> coupons) throws CouponException {
        // status 是EXPIRED, 代表是已过期的优惠券
        // 影响到两个Cache USABLE, EXPIRED
        log.debug("Add Coupon To Cache For Expired.");

        // 最终需要保存的Cache
        HashMap<String, String> needCachedForExpired = new HashMap<>(coupons.size());

        // redisKey
        String redisKeyForUsable = status2RedisKey(
                CouponStatusEnum.USABLE.getCode(), userId);
        String redisKeyForExpired = status2RedisKey(
                CouponStatusEnum.EXPIRED.getCode(), userId);

        // 获取当前用户可用的优惠券
        List<Coupon> curUsableCoupons =
                getCachedCoupons(userId, CouponStatusEnum.USABLE.getCode());

        // 当前可用的优惠券个数一定是大于1的
        assert curUsableCoupons.size() > coupons.size();

        coupons.forEach(c -> needCachedForExpired.put(
                c.getId().toString(), JSON.toJSONString(c)
        ));

        // 校验当前的优惠券参数是否与Cache中的匹配
        List<Integer> curUsableIds = curUsableCoupons.stream()
                .map(Coupon::getId).collect(Collectors.toList());
        List<Integer> paramIds = coupons.stream()
                .map(Coupon::getId).collect(Collectors.toList());

        if (!CollectionUtils.isSubCollection(paramIds, curUsableIds)) {
            log.error("CurCoupons Is Not Equal ToCache: {}, {}, {}",
                    userId, JSON.toJSONString(curUsableIds), JSON.toJSONString(paramIds));
            throw new CouponException("CurCoupons Is Not Equal To Cache!");
        }

        List<String> needCleanKey = paramIds.stream()
                .map(i -> i.toString()).collect(Collectors.toList());

        SessionCallback<Object> sessionCallback = new SessionCallback<Object>() {
            @Override
            public Object execute(RedisOperations redisOperations) throws DataAccessException {
                // 1. 已过期的优惠券Cache缓存添加
                redisOperations.opsForHash().putAll(
                        redisKeyForExpired, needCachedForExpired
                );
                // 2. 可用的优惠券Cache需要清理
                redisOperations.opsForHash().delete(
                        redisKeyForUsable, needCleanKey.toArray()
                );
                // 3. 重置过期时间
                redisOperations.expire(
                        redisKeyForUsable,
                        getRandomExpiredTime(1, 2),
                        TimeUnit.SECONDS
                );
                redisOperations.expire(
                        redisKeyForExpired,
                        getRandomExpiredTime(1, 2),
                        TimeUnit.SECONDS
                );

                return null;
            }
        };
        log.info("Pipeline Exe Result: {}",
                JSON.toJSONString(redisTemplate.executePipelined(sessionCallback)));

        return coupons.size();
    }

    /**
     * 新增已使用的优惠券到Cache中
     * @param userId
     * @param coupons
     * @return
     */
    private Integer addCouponToCacheForUsed(Long userId, List<Coupon> coupons)
            throws CouponException {

        // 如果status是USED, 代表用户操作是使用当前的优惠券, 影响到两个Cache
        // USABLE, USED
        log.debug("Add Coupon To Cache For Used.");
        HashMap<String, String> needCachedForUsed = new HashMap<>(coupons.size());

        String redisKeyForUsable = status2RedisKey(
                CouponStatusEnum.USABLE.getCode(), userId);
        String redisKeyForUsed = status2RedisKey(
                CouponStatusEnum.USED.getCode(), userId);

        // 获取当前用户可用的优惠券
        List<Coupon> curUsableCoupons = getCachedCoupons(
                userId, CouponStatusEnum.USABLE.getCode()
        );
        // 当前可用的优惠券个数一定是大于1的
        assert curUsableCoupons.size() > coupons.size();

        coupons.forEach(c -> needCachedForUsed.put(
                c.getId().toString(),
                JSON.toJSONString(c)
        ));

        // 校验当前的优惠券参数是否与Cache中的匹配
        List<Integer> curUsableCouponsIds = curUsableCoupons
                .stream()
                .map(c -> c.getId())
                .collect(Collectors.toList());
        List<Integer> paramIds = coupons.stream()
                .map(c -> c.getId())
                .collect(Collectors.toList());
        if (!CollectionUtils.isSubCollection(paramIds, curUsableCouponsIds)) {
            log.error("CurCoupons Is Not Equal ToCache: {}, {}, {}",
                    userId, JSON.toJSONString(curUsableCouponsIds),
                    JSON.toJSONString(paramIds));
            throw new CouponException("CurCoupons Is Not Equal To Cache!");
        }

        List<String> needCleanKey = paramIds
                .stream()
                .map(i -> i.toString())
                .collect(Collectors.toList());

        SessionCallback<Object> sessionCallback = new SessionCallback<Object>() {
            @Override
            public Object execute(RedisOperations redisOperations) throws DataAccessException {
                // 1. 已使用的优惠券 Cache 缓存添加
                redisOperations.opsForHash().putAll(
                        redisKeyForUsed, needCachedForUsed
                );
                // 2. 可用的优惠券 Cache 需要清理
                redisOperations.opsForHash().delete(
                        redisKeyForUsable, needCleanKey.toArray()
                );
                // 3. 重置过期时间
                redisOperations.expire(
                        redisKeyForUsed,
                        getRandomExpiredTime(1, 2),
                        TimeUnit.SECONDS
                );
                redisOperations.expire(
                        redisKeyForUsable,
                        getRandomExpiredTime(1, 2),
                        TimeUnit.SECONDS
                );

                return null;
            }
        };
        log.info("Pipeline Exe Result: {}",
                JSON.toJSONString(redisTemplate.executePipelined(sessionCallback)));

        return coupons.size();
    }

    /**
     * 新增加优惠券到Cache中
     * @param userId
     * @param coupons
     * @return
     */
    private Integer addCouponToCacheForUsable(Long userId, List<Coupon> coupons) {
        // 如果status是USABLE, 代表是新增的优惠券
        // 只会影响一个Cache: USER_COUPON_USABLE
        log.debug("Add Coupon To Cache For Usable.");

        HashMap<String, String> needCachedObject = new HashMap<>();
        coupons.forEach(c -> {
            needCachedObject.put(c.getId().toString(),
                    JSON.toJSONString(c));
        });

        String redisKey = status2RedisKey(CouponStatusEnum.USABLE.getCode(), userId);

        redisTemplate.opsForHash().putAll(redisKey, needCachedObject);

        log.info("Add {} Coupons To Cache: {}, {}",
                needCachedObject.size(), userId, redisKey);

        redisTemplate.expire(redisKey,
                getRandomExpiredTime(1, 2),
                TimeUnit.SECONDS);

        return needCachedObject.size();
    }


    /**
     * 根据status获取到对应的Redis Key
     *
     * @param status
     * @param userId
     * @return
     */
    private String status2RedisKey(Integer status, Long userId) {
        String redisKey = null;

        CouponStatusEnum couponStatusEnum = CouponStatusEnum.of(status);

        switch (couponStatusEnum) {
            case USABLE:
                redisKey = String.format("%s%s",
                        Constants.RedisPrefix.USER_COUPON_USABLE, userId);
                break;
            case USED:
                redisKey = String.format("%s%s",
                        Constants.RedisPrefix.USER_COUPON_USED, userId);
                break;
            case EXPIRED:
                redisKey = String.format("%s%s",
                        Constants.RedisPrefix.USER_COUPON_EXPIRED, userId);
                break;
        }

        return redisKey;
    }

    /**
     * 获取一个随机的过期时间
     * @param min 最小的小时数
     * @param max 最大的小时数
     * @return 返回 [min, max] 之间的随机秒数
     */
    private Long getRandomExpiredTime(Integer min, Integer max) {

        return RandomUtils.nextLong(
                min * 60 * 60,
                max * 60 * 60
        );
    }
}
